Skip to content

Add legacy WordPress support on PHP 5.2#3490

Merged
adamziel merged 24 commits intotrunkfrom
legacy-wp-support-php-5.2
Apr 29, 2026
Merged

Add legacy WordPress support on PHP 5.2#3490
adamziel merged 24 commits intotrunkfrom
legacy-wp-support-php-5.2

Conversation

@JanJakes
Copy link
Copy Markdown
Member

@JanJakes JanJakes commented Apr 14, 2026

Stacked on #3501 (Add PHP 5.2 WebAssembly builds and runtime support). Review that PR first.

Summary

Boot legacy WordPress (1.0 – 6.2) in the browser, with SQLite as the database.

Screenshot 2026-04-15 at 13 30 53

The Playground settings panel gains an "Include older versions" checkbox that exposes every WordPress release from 1.0 through 6.9 (with jazz release code names) and auto-locks the PHP version to whatever runs it.

Replaces and closes #3469 (the PHP 5.6 version of the same feature).

What's in this PR

Builds on the PHP 5.2 WASM runtime from #3501 and adds:

  • Offline PHP 7+ → 5.2 downgrader (scripts/php52-downgrader/) — a nikic/PhpParser pipeline of ~20 visitors that strips modern syntax so arbitrary PHP 7-era files parse on PHP 5.2.
  • SQLite plugin patcher (scripts/patch-sqlite-for-php52.mjs) — produces a PHP 5.2-compatible build of the upstream SQLite integration plugin.
  • Legacy WordPress boot flow (wordpress/src/boot.ts, legacy-wp-fixes.ts, mysql-shims.ts) — legacy-aware installer path, PDO table fallback for WP < 3.5, ~3400 lines of targeted source patches for WP 1.0 – 2.8 parser/SQL quirks, auth/nonce compat, wp-db.php SQLite compatibility, and more.
  • WP 5.0–6.2 on PHP 7.4 — extends the legacy boot to cover the mid-modern range where PHP 8.x is unreliable.
  • Site settings UI — "Include older versions" checkbox + grouped dropdown + automatic PHP locking: WP 1.0 – 4.9 → PHP 5.2, WP 5.0 – 6.2 → PHP 7.4, WP 6.3+ → default.
  • Release code names — WP version picker shows jazz-musician shorthand (e.g. "6.9 (Gene)", "4.9 (Tipton)").
  • TinyMCE content_css fix — inlines editor CSS via content_style to work around TinyMCE's document.open() creating a service-worker-uncontrolled iframe.

Test plan

Automated (Playwright, all 32 legacy versions)

npm run dev
# Wait for http://localhost:5400/website-server/ to be ready
node packages/playground/wordpress/tests/test-legacy-wp-version-boot.mjs

Covers front page, single post, admin dashboard, new post page, and plugin activation across every WP minor release from 1.0 to 6.2.

Manual UI walk-through

  1. npm run dev and open http://localhost:5400/.
  2. Open the settings gear → tick Include older versions.
  3. Verify the WP dropdown shows two groups and every release from 1.0 to 6.9.
  4. Pick WP 4.9 → PHP locks to 5.2 (disabled).
  5. Pick WP 5.5 → PHP locks to 7.4 (disabled).
  6. Pick WP 4.8, go to Posts → Add New, verify TinyMCE loads without CSS errors.

🤖 Generated with Claude Code

@JanJakes JanJakes force-pushed the legacy-wp-support-php-5.2 branch 5 times, most recently from d2b37ed to a0865df Compare April 15, 2026 11:48
@JanJakes JanJakes marked this pull request as ready for review April 15, 2026 11:52
@JanJakes JanJakes requested review from a team, adamziel, Copilot and mho22 April 15, 2026 11:52
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds first-class legacy WordPress (1.0–4.9) support by introducing a PHP 5.2 WASM runtime, a PHP 7+→5.2 downgrader pipeline for the SQLite integration plugin, and end-to-end boot/install fixes plus UI + CI coverage to select and validate older WordPress versions.

Changes:

  • Added PHP 5.2 runtimes (node + web), wiring them into loaders and version typing (AllPHPVersion).
  • Implemented an AST-based PHP 7+→5.2 downgrader (many visitors + pretty printer) and bundled a prepatched SQLite plugin zip for PHP 5.2.
  • Updated Playground boot flow, worker downloads, and website settings UI to support “Include older versions” + automatic PHP version locking; added Playwright coverage and CI job.

Reviewed changes

Copilot reviewed 82 out of 99 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
tsconfig.base.json Adds TS path aliases for new PHP 5.2 node/web build packages.
scripts/php52-downgrader/src/Visitor/VariadicAndSplatVisitor.php Downgrades variadics and splat calls to PHP 5.2-compatible forms.
scripts/php52-downgrader/src/Visitor/StripTypeDeclarationsVisitor.php Removes PHP 7+ type declarations for PHP 5.2 compatibility.
scripts/php52-downgrader/src/Visitor/ShortTernaryVisitor.php Lowers ?: short ternary to full ternary with temp vars when needed.
scripts/php52-downgrader/src/Visitor/SelfParentStaticRewriter.php Rewrites self/parent/static references when hoisting code out of class scope.
scripts/php52-downgrader/src/Visitor/ReservedMethodRenameVisitor.php Renames reserved/invalid method identifiers to keep output parseable.
scripts/php52-downgrader/src/Visitor/PromoteForHoistedClosuresVisitor.php Promotes member visibility to support hoisted-closure $this access on PHP 5.2.
scripts/php52-downgrader/src/Visitor/Php7ErrorClassesVisitor.php Maps PHP 7 error classes/Throwable to Exception.
scripts/php52-downgrader/src/Visitor/NullsafeVisitor.php Lowers PHP 8 nullsafe operator to ternary + temps.
scripts/php52-downgrader/src/Visitor/NullCoalescingVisitor.php Lowers ?? and ??= into PHP 5.2-compatible forms.
scripts/php52-downgrader/src/Visitor/NamespaceStripVisitor.php Strips namespaces and leading \ in names for PHP 5.2 parsing.
scripts/php52-downgrader/src/Visitor/LateStaticBindingVisitor.php Rewrites static:: to self:: for PHP 5.2 (drops LSB).
scripts/php52-downgrader/src/Visitor/InstanceCallOnNewVisitor.php Rewrites (new Foo())->method/prop access into helper calls.
scripts/php52-downgrader/src/Visitor/FinallyVisitor.php Lowers finally blocks by duplicating finally body into try/catches.
scripts/php52-downgrader/src/Visitor/ExponentVisitor.php Rewrites ** and **= to pow(...).
scripts/php52-downgrader/src/Visitor/DirConstantVisitor.php Forces __DIR__ into dirname(__FILE__) before later rewrites.
scripts/php52-downgrader/src/Visitor/ClosureHoistingVisitor.php Hoists closures/arrow functions into named functions with capture handling.
scripts/php52-downgrader/src/Visitor/ClassKeywordVisitor.php Rewrites ::class magic constant into compatible expressions.
scripts/php52-downgrader/src/Visitor/CallableExprVisitor.php Rewrites complex callable expressions into call_user_func(...).
scripts/php52-downgrader/src/Visitor/AttributeAndDeclareStripVisitor.php Removes PHP 8 attributes and declare(strict_types=1) for PHP 5.2.
scripts/php52-downgrader/src/Visitor/ArrayDerefOnCallVisitor.php Rewrites fn()[0] patterns to helper _pg52_at(...).
scripts/php52-downgrader/src/Visitor/ArrayClassConstantVisitor.php Hoists non-const class constant initializers into static properties + runtime init.
scripts/php52-downgrader/src/PrettyPrinter.php Forces PHP 5.2-friendly printing (long arrays, __DIR__ safety net).
scripts/php52-downgrader/src/Downgrader.php Orchestrates the visitor pipeline + cross-file constant hoist discovery.
scripts/php52-downgrader/composer.json Adds composer package for the downgrader toolchain.
scripts/php52-downgrader/.gitignore Ignores Composer vendor directory for the new tool.
packages/playground/wordpress/tests/test-legacy-wp-version-boot.mjs Adds Playwright test covering boot flows across WP 1.0–4.9.
packages/playground/wordpress/src/mysql-shims.ts Introduces mysql_* stubs for very old WP versions using SQLite.
packages/playground/wordpress/src/boot.ts Adds legacy boot/install flow, network disabling, and safe DB checks for PHP < 7.
packages/playground/wordpress/project.json Adds an Nx target to run the legacy boot test.
packages/playground/wordpress-builds/src/sqlite-database-integration/get-sqlite-driver-module-details.ts Adds module details for the prepatched v2.2.22-php52 SQLite driver asset.
packages/playground/website/src/lib/state/redux/site-management-api-middleware.ts Switches PHP version typing to AllPHPVersion to include legacy versions.
packages/playground/website/src/lib/state/opfs/opfs-site-storage.ts Updates persisted site metadata typing to allow legacy PHP versions.
packages/playground/website/src/components/site-manager/site-settings-form/unconnected-site-settings-form.tsx Adds “Include older versions” toggle + grouped WP dropdown + PHP version locking.
packages/playground/website/src/components/site-manager/site-settings-form/older-wordpress-versions.ts Adds older WP version list and a mapping to forced PHP versions.
packages/playground/remote/src/lib/playground-worker-endpoint.ts Forces single-instance mode for legacy PHP; uses PHP 5.2 MU plugin stub.
packages/playground/remote/src/lib/playground-worker-endpoint-blueprints-v1.ts Enables wordpress.org zip downloads for non-minified versions and selects v2.2.22-php52 for legacy PHP.
packages/playground/remote/src/lib/playground-mu-plugin/playground-includes/wp_http_fetch.php Replaces null coalescing with PHP 5.2-safe isset ternary.
packages/playground/remote/src/lib/playground-mu-plugin/0-playground.php Adds guards for older PHP/WP and avoids PHP 5.4+ array shorthand.
packages/playground/remote/src/lib/playground-mu-plugin/0-playground-php52.php Adds a minimal PHP 5.2-compatible MU plugin stub (dummy HTTP transport + cron disable).
packages/playground/client/src/blueprints-v1-handler.ts Skips update-check prefetch on legacy WordPress versions.
packages/playground/cli/tests/run-cli.spec.ts Adjusts CLI test versions to avoid legacy WP on modern PHP.
packages/playground/cli/src/run-cli.ts Allows selecting legacy PHP via AllPHPVersions and gates extensions/debug defaults for legacy PHP.
packages/playground/cli/src/blueprints-v1/worker-thread-v1.ts Passes phpVersion through to bootWordPress in the CLI worker.
packages/playground/cli/src/blueprints-v1/download.ts Adds ability to pick specific sqlite driver zip variants (incl -php52).
packages/playground/cli/src/blueprints-v1/blueprints-v1-handler.ts Chooses prepatched sqlite driver version for legacy PHP.
packages/playground/blueprints/src/lib/v1/types.ts Updates blueprint PHP version typing to include legacy versions.
packages/playground/blueprints/src/lib/v1/compile.ts Compiles blueprint php version using AllPHPVersions.
packages/playground/blueprints/src/lib/types.ts Updates runtime config PHP typing to AllPHPVersion.
packages/playground/blueprints/public/blueprint-schema.json Adds AllPHPVersion/LegacyPHPVersion to schema to expose PHP 5.2.
packages/php-wasm/web/src/lib/load-runtime.ts Allows loading legacy PHP in web, writes pre-boot php.ini via preRun, and blocks intl for legacy.
packages/php-wasm/web/src/lib/get-php-loader-module.ts Adds loader module resolution for PHP 5.2 web build.
packages/php-wasm/web-builds/5-2/tsconfig.lib.json Adds TS build configuration for PHP 5.2 web build package.
packages/php-wasm/web-builds/5-2/tsconfig.json Adds TS project config for PHP 5.2 web build package.
packages/php-wasm/web-builds/5-2/src/index.ts Adds 5.2 loader that selects JSPI vs Asyncify at runtime.
packages/php-wasm/web-builds/5-2/project.json Adds Nx project/targets for building/publishing PHP 5.2 web binaries.
packages/php-wasm/web-builds/5-2/package.json Adds npm package metadata for @php-wasm/web-5-2.
packages/php-wasm/web-builds/5-2/build.js Adds esbuild-based bundle step for web PHP 5.2 package.
packages/php-wasm/universal/src/lib/supported-php-versions.ts Introduces LegacyPHPVersions, AllPHPVersions, and isLegacyPHPVersion.
packages/php-wasm/universal/src/lib/proxy-file-system.ts Skips PROXYFS mmap patching for legacy PHP to avoid parser corruption.
packages/php-wasm/universal/src/lib/legacy-php-ini.ts Adds pre-boot php.ini content + preRun writer for legacy PHP builds.
packages/php-wasm/universal/src/lib/index.ts Exports new legacy version helpers and legacy php.ini utility.
packages/php-wasm/node/src/lib/load-runtime.ts Enables legacy PHP loading on node; blocks extensions on legacy; adds preRun ini creation.
packages/php-wasm/node/src/lib/get-php-loader-module.ts Adds loader module resolution for PHP 5.2 node build.
packages/php-wasm/node/src/lib/extensions/memcached/get-memcached-extension-module.ts Formatting-only changes in extension module resolution.
packages/php-wasm/node-builds/5-2/tsconfig.lib.json Adds TS build configuration for PHP 5.2 node build package.
packages/php-wasm/node-builds/5-2/tsconfig.json Adds TS project config for PHP 5.2 node build package.
packages/php-wasm/node-builds/5-2/src/index.ts Adds 5.2 loader that selects JSPI vs Asyncify at runtime.
packages/php-wasm/node-builds/5-2/project.json Adds Nx project/targets for building/publishing PHP 5.2 node binaries.
packages/php-wasm/node-builds/5-2/package.json Adds npm package metadata for @php-wasm/node-5-2.
packages/php-wasm/node-builds/5-2/build.js Adds esbuild-based bundle step for node PHP 5.2 package.
packages/php-wasm/compile/php/proc_open.h Adds guards and PHP 5/7-compatible pipe typing for proc_open integration.
packages/php-wasm/compile/php/proc_open.c Adds PHP 5.x stub implementation and PHP 5/7-compatible includes.
packages/php-wasm/compile/php/php_wasm.c Adds PHP 5/7 API compatibility shims for string APIs and SAPI function signatures.
packages/php-wasm/compile/php/apply-mysqlnd-patch.sh Makes mysqlnd patch application conditional by PHP version/file availability.
packages/php-wasm/compile/php/Dockerfile Updates build logic to support PHP 5.x differences (zip/sqlite/curl/imagick/fiber/asm guards, warning flags).
packages/php-wasm/compile/php-wasm-memory-storage/wasm_memory_storage.c Disables custom memory storage implementation for PHP < 7 with no-op init/shutdown.
packages/php-wasm/compile/php-wasm-dns-polyfill/dns_polyfill.c Adds PHP 5/7 signature/parameter parsing compatibility.
packages/php-wasm/compile/php-post-message-to-js/post_message_to_js.c Adds PHP 5/7 parameter parsing and return-string API compatibility.
.github/workflows/ci.yml Adds CI job to boot-test legacy WP versions using Playwright.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread scripts/php52-downgrader/src/Visitor/ClosureHoistingVisitor.php Outdated
Comment thread scripts/php52-downgrader/src/Visitor/ArrayClassConstantVisitor.php Outdated
Comment on lines +276 to +299
private function isPhp52ConstantExpr(Node\Expr $expr): bool
{
if ($expr instanceof Node\Scalar\Int_) {
return true;
}
if ($expr instanceof Node\Scalar\Float_) {
return true;
}
if ($expr instanceof Node\Scalar\String_) {
return true;
}
if ($expr instanceof Expr\ConstFetch) {
$name = $expr->name->toString();
return in_array(
$name,
['true', 'false', 'null', 'TRUE', 'FALSE', 'NULL'],
true
);
}
if ($expr instanceof Expr\UnaryMinus || $expr instanceof Expr\UnaryPlus) {
return $this->isPhp52ConstantExpr($expr->expr);
}
return false;
}
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docblock says PHP 5.2 class constants may reference other class constants, but isPhp52ConstantExpr() currently treats all Expr\\ClassConstFetch as non-constant and will hoist them into runtime-initialized static properties. That changes semantics and can break valid constant usages (e.g., switch cases, array keys) that require a real constant. Consider extending isPhp52ConstantExpr() to treat Expr\\ClassConstFetch (at least for non-self/static/parent or other safe cases) as a valid PHP 5.2 constant expression so those constants remain compile-time constants.

Copilot uses AI. Check for mistakes.
Comment on lines +181 to +224
/** Returns the callable expression (first arg to call_user_func_array). */
private function buildCallable(Node $node): ?Expr
{
if ($node instanceof Expr\FuncCall) {
if ($node->name instanceof Node\Name) {
return new Node\Scalar\String_($node->name->toString());
}
if ($node->name instanceof Expr) {
return $node->name;
}
return null;
}
if ($node instanceof Expr\MethodCall) {
$method = $node->name instanceof Node\Identifier
? $node->name->toString()
: null;
if ($method === null) {
return null;
}
return new Expr\Array_([
new Node\ArrayItem($node->var),
new Node\ArrayItem(new Node\Scalar\String_($method)),
]);
}
if ($node instanceof Expr\StaticCall) {
$method = $node->name instanceof Node\Identifier
? $node->name->toString()
: null;
if ($method === null) {
return null;
}
$cls = $node->class instanceof Node\Name
? $node->class->toString()
: null;
if ($cls === null) {
return null;
}
return new Expr\Array_([
new Node\ArrayItem(new Node\Scalar\String_($cls)),
new Node\ArrayItem(new Node\Scalar\String_($method)),
]);
}
return null;
}
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For method/static calls with dynamic names (e.g. $obj->{$m}(...$args) or Foo::{$m}(...$args)), buildCallable() currently returns null, which causes rewriteSplatCall() to bail out and leave the unpack (...) syntax in the output—resulting in PHP 5.2 parse errors. Instead of returning null for dynamic identifiers, consider building a callable array with the method expression as-is, and for StaticCall also allow $node->class forms beyond Node\\Name (e.g. variables) when they can be passed through to call_user_func_array.

Copilot uses AI. Check for mistakes.
Comment thread scripts/php52-downgrader/src/Visitor/SelfParentStaticRewriter.php Outdated
Comment thread packages/php-wasm/node/src/lib/load-runtime.ts
Comment thread packages/playground/wordpress/src/boot.ts Outdated
Comment thread packages/playground/wordpress/src/boot.ts Outdated
Comment thread scripts/php52-downgrader/src/Visitor/ClosureHoistingVisitor.php Outdated
Comment thread packages/php-wasm/universal/src/lib/proxy-file-system.ts
@JanJakes JanJakes force-pushed the legacy-wp-support-php-5.2 branch from a0865df to efa1168 Compare April 15, 2026 20:25
@JanJakes JanJakes changed the base branch from trunk to php-5.2-wasm April 16, 2026 11:51
@JanJakes JanJakes force-pushed the legacy-wp-support-php-5.2 branch 2 times, most recently from f4e6cbd to 91853e3 Compare April 16, 2026 12:08
@JanJakes JanJakes force-pushed the legacy-wp-support-php-5.2 branch from 91853e3 to 5b6a80c Compare April 16, 2026 12:18
@JanJakes JanJakes force-pushed the legacy-wp-support-php-5.2 branch from 5b6a80c to a91d005 Compare April 16, 2026 12:30
@JanJakes JanJakes force-pushed the legacy-wp-support-php-5.2 branch from a91d005 to 106086f Compare April 16, 2026 12:44
@JanJakes JanJakes force-pushed the legacy-wp-support-php-5.2 branch from 106086f to c1e7651 Compare April 16, 2026 15:47
Base automatically changed from php-5.2-wasm to trunk April 16, 2026 16:13
JanJakes added 18 commits April 28, 2026 13:28
Everything a legacy WordPress / legacy PHP setup needs now lives
under packages/playground/wordpress/src/legacy-wp/:

  * legacy-boot.ts — bootLegacyWordPress(), the self-contained
    entry point mirroring bootWordPress's step ordering for PHP
    5.2 + WP 1.0–4.9 on SQLite. Also hosts applyLegacyPhpIniOverrides
    (called from bootRequestHandler).
  * legacy-fixes.ts — WordPress source-file patches
    (patchWordPressSourceFiles, backportWpPreV62MysqlCheck),
    generateDbPhpContent, runPostInstallLegacyFixups, and the
    LEGACY_WP_ERROR_REPORTING_* constants.
  * mysql-shims.ts — PHP shims for mysql_* / mysqli_* APIs that
    old WP expects.

boot.ts stays modern-first: a single
`if (isLegacyPHPVersion(options.phpVersion)) return bootLegacyWordPress(...)`
dispatches at the top of bootWordPress, and the body has zero
legacy conditionals. assertValidDatabaseConnection no longer takes
a phpVersion param — only the modern path reaches it. The
installWordPressSafe + assertValidDatabaseConnectionSafe wrappers
are gone; each boot path owns its own error handling.

Also:

  * Gate backportWpPreV62MysqlCheck explicitly to WP 5.0–6.1 (renamed
    from patchLegacyMysqlCheckForModernWp); skip the placeholder
    db.php write on WP 6.2+ so modern trunk behaviour is preserved.
  * Replace parseInt(phpVersion, 10) < 7 checks with
    isLegacyPHPVersion() so non-numeric values like 'latest' never
    slip into the legacy branch.
…lder

Extract every legacy mu-plugin and SQLite preload setup from
index.ts into dedicated files under legacy-wp/:

  * legacy-mu-plugins.ts — setupLegacyPlatformLevelMuPlugins,
    LEGACY_AUTO_LOGIN_BODY, legacy preload env.php, PHP 5.2
    error handler, legacy auto_prepend_file.
  * legacy-sqlite-preload.ts — preloadLegacySqliteIntegration,
    buildLegacySqlitePreload, mysql*/mysqli* stubs, str_*
    polyfills, WP < 3.1 add_action() guard.

Shared helpers live in neutral files at src/ root so legacy-wp/
doesn't import from the parent index.ts:

  * platform-mu-plugins.ts — writeCommonPlatformMuPlugins
    (0-playground.php, sitemap-redirect.php,
    inline-tinymce-content-css.php), called from both paths.
  * sqlite-preload-loader.ts — SQLITE_PRELOAD_LOADER_CLASS,
    referenced by both modern and legacy preload builders.

setupPlatformLevelMuPlugins and preloadSqliteIntegration now
dispatch to the legacy helpers at the top and their bodies carry
no phpMajor / isPhp52 branches. index.ts goes from 1521 lines to
691.

Also drop the WP 5.0–6.1 db.php placeholder.
backportWpPreV62MysqlCheck used to write an empty
wp-content/db.php so install.php step=2's \$mysql_compat check
would fall through its file_exists(WP_CONTENT_DIR . '/db.php')
escape hatch. The placeholder was never needed —
WP_SQLite_DB::db_version() returns '8.0' (per the SQLite plugin's
docstring: "it returns mysql version number, but it means nothing
for SQLite. So it returns the newest mysql version"), which
satisfies install.php's version_compare check directly via the
lazy-\$wpdb loader. With this change no file is ever written to
wp-content on the modern boot path.
Reviewed every patch in legacy-wp/legacy-fixes.ts (and supporting files)
for need, gating tightness, and comment clarity. Net: 4828 → 3637 lines.
Verified WP 1.0–4.9 + PHP 5.2 still boot end-to-end.

Notable changes:
- Drop ensureLegacyAdminAuth — duplicated the auto-login flow already
  provided by legacy-mu-plugins.ts.
- Drop the wp_check_mysql_version no-op — mysql-shims.ts already
  returns '8.0.0' from mysql_get_server_info.
- Drop the dead overrides array in patchWpAdminRelativePaths — the
  generic regex pass already covers every entry.
- Drop duplicate mysql_/mysqli_/str_* stubs from generateDbPhpContent —
  the 0-sqlite.php preload runs first and already defines them.
- Promote `mail` to LEGACY_PHP_DISABLED_NETWORK_FUNCTIONS so it's
  blocked across the whole boot lifecycle, and drop the wp_mail no-op
  in patchWpInstallMailCrash (PHPMailer's SMTP fallback also dies on
  the disabled fsockopen, so it now fails safely with a WP_Error).
- Drop the dead collapseWp1xInstallerSteps helper — install.php is
  never reached during legacy boot, and runPostInstallLegacyFixups
  seeds the admin user via PDO so a manual visit short-circuits to
  "Already Installed".
- Drop the dead `if (!\$_pg_skip_redirect)` redirect branches from all
  three eras of LEGACY_AUTO_LOGIN_BODY — playground_load_mu_plugins
  unconditionally defines PLAYGROUND_SKIP_AUTO_LOGIN_REDIRECT before
  invoking the body in legacy contexts.
- Restrict playground_legacy_set_auth_cookies_early to WP < 2.5; the
  wordpressuser_* cookies it sets are inert on later versions.
- Share legacyAuthCookieBlock(usernameExpr) between
  patchAdminAuthRedirect and patchAdminAjaxAuth; replace their brittle
  wp-settings.php text scans with readOnDiskWpVersion gates.
- Move every WP-version-specific gate out of the patch functions and
  into patchWordPressSourceFiles — the orchestrator now reads the WP
  version once and dispatches each patch only when its range matches.
  The previously needle-only patches (e.g. patchWp10*, patchWp21*,
  patchCheckAdminReferer) get explicit ranges too, so they short-circuit
  at the front door instead of reading files they can't apply to.
- Replace the fragile one-level-nesting regex in patchCheckAdminReferer
  with a reusable balanced-brace helper (replacePhpFunctionBody), reused
  by the "not installed" die() and the do_action('init') rewrite
  extracted out of patchWpSettingsPhp.
- Fix patchWpSettingsPhp bug where settingsChanged was set
  unconditionally inside the error_reporting block, rewriting
  wp-settings on every boot.
- Split the PHP 5.2 vs 5.3+ error-handler boilerplate in
  legacy-mu-plugins.ts into named helpers instead of inline ternaries.
- Trim multi-paragraph "## The bug / ## The fix" docblocks to a single
  why-sentence each, dropping embedded WP source samples and
  indentation diagrams.
Spell out in the top-of-file doc block that the legacy patches run
as plain, idempotent string replacements without parsing PHP, why
this relies on legacy WP releases being frozen, and point at the
boot-smoke suite as the safety net for changes here.
Guards against Playground writing its own db.php, object-cache.php,
advanced-cache.php, or sunrise.php into a user-mounted wp-content.
Studio and similar consumers mount real wp-content directories
into Playground, and a stray Playground-managed drop-in there would
silently take over the user's external site.
Two targeted changes that remove ~2/3 of the per-version wall time:

- Drop waitForWPFrame's 3 s poll granularity down to 500 ms, so the
  loop reflects how fast Playground actually responds instead of
  always burning whole 3 s ticks.
- Replace the two unconditional waitForTimeout(8000) waits after
  post-click (Phase 2) and plugin-activate click (Phase 5) with an
  event-based wait: waitForWPFrame now accepts `excludeUrl` (skip
  the scoped frame while its URL still matches the pre-click value)
  and `contentPredicate` (require specific outcome text before
  returning, so we don't race the intermediate admin shell between
  POST and post-redirect render).

Phase 5 also explicitly waits for any Activate link to become
visible before selecting one — navigateViaUrlBar returns as soon
as plugins.php has any body text, which on slow CI boots can be
just the admin shell before the plugin list is populated.

Local run on 7 representative versions (6.2, 6.1, 6.0, 5.9, 5.8,
4.9, 3.2) goes from ~33 s/version to ~10 s/version, all 5 phases
pass including the ones that flaked under the prior code.
The SQLite integration plugin ships with MySQL 8 strict defaults
(NO_ZERO_DATE + STRICT_TRANS_TABLES + ONLY_FULL_GROUP_BY), but
legacy WordPress (1.0–4.9, contemporary with MySQL 4.1–5.6) was
written against empty or near-empty sql_mode defaults. Three
`patchWp*ZeroDate*` helpers were previously patching every
wp_insert_post() variant that emits '0000-00-00 00:00:00' for
drafts, just to make those INSERTs pass.

Reset the driver's default `active_sql_modes` to `array()` once
during SQLite preload for legacy PHP boots. One regex on the
driver source, two source variants (standard multi-line and the
php52-downgraded single-line form) covered by the same needle.
WP 3.9+ already achieves the same effect at runtime via
wpdb::set_sql_mode(), so the relaxed driver default is a no-op
there and strictly an improvement for WP < 3.9.

Net: -213 lines in legacy-fixes.ts (three patch functions +
their needles), +39 in legacy-sqlite-preload.ts. Smoke test
across WP 1.5 / 2.1 / 2.5 / 2.8 / 3.2 / 3.5 / 4.0 / 4.1 all
pass on PHP 5.2.
Phase 3 (wp-admin dashboard load via URL bar) has been flaking on
shared CI runners for one random modern-WP version per run — 5.5,
5.9, 6.0 across recent runs, always with the same empty-detail
`admin [TIMEOUT]` after the full 120 s navigateViaUrlBar budget.
The iframe gets stuck partway into the first /wp-admin/ load and
never progresses; a fresh URL-bar fill+Enter almost always unsticks
it on the second try.

Retry once on timeout inline. Cheap and keeps real admin failures
visible — the retry only kicks in when the first attempt returned
null, so a genuine admin-page error still reports as the specific
failure mode (ERROR / UNKNOWN) rather than getting retried.
Three provably-dead snippets removed from the legacy boot path:

- PLAYGROUND_SKIP_AUTO_LOGIN_REDIRECT: defined in env.php but never
  read anywhere. LEGACY_AUTO_LOGIN_BODY has no redirect branch that
  would consult it. Delete the define() and collapse the misleading
  docblock to describe what the code actually does.

- waitForAdminFrame(): 47-line helper in the legacy boot smoke test,
  never called. Phase 3 uses navigateViaUrlBar + waitForWPFrame.

- str_contains/str_starts_with/str_ends_with polyfills in the legacy
  SQLite preload: the downgraded v2.2.22-php52 plugin ships identical
  function_exists-guarded polyfills in its own php-polyfills.php, and
  no code between the preload and the lazy-wpdb first touch calls
  str_*. The preload copies were redundant.

playground-wordpress build + lint + 80/80 unit tests stay green.
env.php's playground_legacy_set_auth_cookies_early runs via
auto_prepend_file before every PHP script (including the source-
patched wp-admin/admin.php and wp-admin/admin-ajax.php). For WP <
2.5 it already populates wordpressuser_/wordpresspass_ cookies,
which are identical to the USER_COOKIE/PASS_COOKIE constants
WP 1.5–2.4 reads (both resolve to wordpressuser_\$cookiehash /
wordpresspass_\$cookiehash). So the USER_COOKIE and COOKIEHASH
elseif branches of legacyAuthCookieBlock were re-setting cookies
that were already there.

Keep the HMAC branch (WP 2.5+) and the \$_pg_user=null default;
drop both elseif branches. Smoke test across WP 1.2 / 2.1 / 2.5 /
4.9 / 6.2 unchanged.
The legacy error handler had two builders — buildPhp52ErrorHandler
(named function + \$GLOBALS) and buildModernErrorHandler (closure
wrapped in call_user_func) — chosen by isPhp52. The named-function
form works on every PHP version from 5.2 through 8.x, so the split
is unnecessary.

Drop buildModernErrorHandler and the isPhp52 flag; emit the
named-function form unconditionally. setupLegacyPlatformLevelMuPlugins
no longer needs its phpVersion option.
Neither has anything to do with SQLite; both were sitting at the
tail of buildLegacySqlitePreload for historical reasons.

 * SERVER_PROTOCOL default moves to the legacy auto_prepend_file
   (next to the other \$_SERVER / register_globals polyfills), so
   it's set before preloads run and on every entry point — HTTP
   requests, php.run() fixups, CLI scripts.

 * date.timezone moves into applyLegacyPhpIniOverrides as an ini
   entry, guarded so a caller-supplied value still wins.

No runtime behaviour change on any tested WP version; smoke test
across WP 1.2 / 2.1 / 2.5 / 4.9 / 6.2 still green.
…IMS_PHP

The four connection stubs that legacy WordPress (<3.0) needs for
wpdb::__construct to not bail() on a falsy return were inlined at
the top of buildLegacySqlitePreload, while the rest of the mysql_*
family lived in MYSQL_SHIMS_PHP. Split by historical accident, not
by design.

Move the four connection stubs into MYSQL_SHIMS_PHP at the top
(connection stubs first, then query/result stubs). The preload
emits the same PHP output as before, but there's now one source of
truth in mysql-shims.ts for every mysql_*/mysqli_* shim.
The newly-tagged v3.0.0-rc.3 release ships AUTO_INCREMENT value
management (sqlite-database-integration#367):

  * SHOW TABLE STATUS LIKE returns a real Auto_increment computed
    from sqlite_sequence (was NULL).
  * INSERT … VALUES('0', …) into AUTO_INCREMENT columns is rewritten
    to NULLIF(CAST(… AS INTEGER), 0), so MySQL's "0 advances the
    sequence" semantics work on SQLite.
  * AUTO_INCREMENT = N table option support.

These improvements obviate several legacy-WP source patches we
maintained against v2.2.22's behaviour. Bring them into the legacy
PHP 5.2 boot path by retargeting the offline downgrader at the new
upstream zip and renaming the produced artefact accordingly.

Mechanical: SRC_ZIP/OUT_ZIP in patch-sqlite-for-php52.mjs, the
'v2.2.22-php52' literal in the worker endpoint and CLI handler,
the version union in download.ts, and the resolver entry in
get-sqlite-driver-module-details.ts.

Follow-up: drop the now-redundant patches in legacy-fixes.ts.
WP 1.0's wp-admin/post.php emits `INSERT … VALUES ('0', …)` into the
AUTO_INCREMENT `ID` column. v2.2.22 stored the literal 0; we rewrote
the source to NULL so SQLite would pick the next rowid.

v3.0.0-rc.3's INSERT translator now wraps every AUTO_INCREMENT value
in `NULLIF(CAST(… AS INTEGER), 0)` (unless NO_AUTO_VALUE_ON_ZERO is
active). The 0-literal advances the sequence on its own, so the
source patch is a no-op.

Verified: WP 1.0 boot smoke test still PASS on front / post /
admin / new post phases.
Same story as the WP 1.0 patch one commit back: WP 1.2's
wp-admin/post.php inserts a literal '0' into the AUTO_INCREMENT
ID column, and v3.0.0-rc.3 now NULLIFs it transparently.

Verified: WP 1.2 boot smoke test PASS on all five phases (front,
post, admin, new post, plugin activation).
WP 2.0-2.2 install.php / upgrade-functions.php / admin-db.php
inserts the seed category as `INSERT INTO wp_categories (…, cat_ID,
…) VALUES ('0', …)`. v2.2.22 stored the literal 0 and the resulting
cat_ID=0 / category_parent=0 self-loop hung get_nested_categories()
in an infinite recursion.

v3.0.0-rc.3 NULLIFs zero-valued AUTO_INCREMENT inserts at the SQL
translator layer, so cat_ID becomes the next sequence value as on
MySQL. The rewriting source patch is no longer required.

Verified: WP 2.0 / 2.1 / 2.2 boot smoke tests all PASS on the five
phases (front, post, admin, new post, plugin activation), so
neither the dashboard nor the categories admin recurses any more.
The patch did two things, both now redundant with v3.0.0-rc.3:

1. SHOW TABLE STATUS LIKE 'wp_posts' previously returned
   Auto_increment = NULL on SQLite, so we backfilled with a
   MAX(ID)+1 fallback. v3.0.0-rc.3 reads the value from
   sqlite_sequence and returns the real next-value, so
   $id_result->Auto_increment is populated already.

2. WP 1.5's $postquery INSERT omits `pinged` and `post_content_filtered`,
   which are NOT NULL in the SQLite-built schema. v3.0.0-rc.3's INSERT
   translator fills omitted NOT NULL columns with their IMPLICIT
   DEFAULT under non-strict mode (we already clear active_sql_modes
   via relaxSqliteDriverSqlModes in the legacy preload), so the
   handwritten `''` columns are no longer required.

Verified: WP 1.5 boot smoke test PASS on all five phases.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants